/*******************************************************************************
 * Copyright (c) 2006, 2015 Wind River Systems and others.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 * 
 * Contributors:
 *     Wind River Systems - initial API and implementation
 *     Ericsson	AB		  - Modified for handling of multiple threads
 *******************************************************************************/
package org.eclipse.cdt.examples.dsf.pda.service;

import java.util.Arrays;
import java.util.Hashtable;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

import org.eclipse.cdt.dsf.concurrent.DataRequestMonitor;
import org.eclipse.cdt.dsf.concurrent.IDsfStatusConstants;
import org.eclipse.cdt.dsf.concurrent.Immutable;
import org.eclipse.cdt.dsf.concurrent.RequestMonitor;
import org.eclipse.cdt.dsf.datamodel.AbstractDMEvent;
import org.eclipse.cdt.dsf.datamodel.IDMEvent;
import org.eclipse.cdt.dsf.debug.service.IRunControl;
import org.eclipse.cdt.dsf.debug.service.command.IEventListener;
import org.eclipse.cdt.dsf.service.AbstractDsfService;
import org.eclipse.cdt.dsf.service.DsfServiceEventHandler;
import org.eclipse.cdt.dsf.service.DsfSession;
import org.eclipse.cdt.examples.dsf.pda.PDAPlugin;
import org.eclipse.cdt.examples.dsf.pda.service.commands.AbstractPDACommand;
import org.eclipse.cdt.examples.dsf.pda.service.commands.PDACommandResult;
import org.eclipse.cdt.examples.dsf.pda.service.commands.PDAResumeCommand;
import org.eclipse.cdt.examples.dsf.pda.service.commands.PDAStepCommand;
import org.eclipse.cdt.examples.dsf.pda.service.commands.PDAStepReturnCommand;
import org.eclipse.cdt.examples.dsf.pda.service.commands.PDASuspendCommand;
import org.eclipse.cdt.examples.dsf.pda.service.commands.PDAVMResumeCommand;
import org.eclipse.cdt.examples.dsf.pda.service.commands.PDAVMSuspendCommand;
import org.osgi.framework.BundleContext;


/**
 * Service for monitoring and controlling execution state of the DPA 
 * program.
 * <p>
 * This service depends on the {@link PDACommandControl} service.
 * It must be initialized before this service is initialized.
 * </p>
 */
public class PDARunControl extends AbstractDsfService 
    implements IRunControl, IEventListener
{
    // Implementation note about tracking execution state:
    // This class implements event handlers for the events that are generated by 
    // this service itself.  When the event is dispatched, these handlers will
    // be called first, before any of the clients.  These handlers update the 
    // service's internal state information to make them consistent with the 
    // events being issued.  Doing this in the handlers as opposed to when 
    // the events are generated, guarantees that the state of the service will
    // always be consistent with the events.
    // The purpose of this pattern is to allow clients that listen to service 
    // events and track service state, to be perfectly in sync with the service
    // state.

    static final private IExecutionDMContext[] EMPTY_TRIGGERING_CONTEXTS_ARRAY = new IExecutionDMContext[0];  

    @Immutable 
    private static class ThreadResumedEvent extends AbstractDMEvent<IExecutionDMContext> 
        implements IResumedDMEvent
    {
        private final StateChangeReason fReason;

        ThreadResumedEvent(PDAThreadDMContext ctx, StateChangeReason reason) { 
            super(ctx);
            fReason = reason;
        }
        
        @Override
	public StateChangeReason getReason() {
            return fReason;
        }
        
        @Override
        public String toString() {
            return "THREAD RESUMED: " + getDMContext() + " (" + fReason + ")"; 
        }
    }
    
    @Immutable 
    private static class VMResumedEvent extends AbstractDMEvent<IExecutionDMContext> 
        implements IContainerResumedDMEvent
    {
        private final StateChangeReason fReason;
        
        VMResumedEvent(PDAVirtualMachineDMContext ctx, StateChangeReason reason) { 
            super(ctx);
            fReason = reason;
        }

        @Override
	public StateChangeReason getReason() {
            return fReason;
        }

        @Override
	public IExecutionDMContext[] getTriggeringContexts() {
            return EMPTY_TRIGGERING_CONTEXTS_ARRAY;
        }
        
        @Override
        public String toString() {
            return "VM RESUMED: (" + fReason + ")"; 
        }
    }
    
    @Immutable
    private static class ThreadSuspendedEvent extends AbstractDMEvent<IExecutionDMContext> 
        implements ISuspendedDMEvent
    {
        private final StateChangeReason fReason;

        ThreadSuspendedEvent(PDAThreadDMContext ctx, StateChangeReason reason) { 
            super(ctx);
            fReason = reason;
        }
        
        @Override
	public StateChangeReason getReason() {
            return fReason;
        }

        @Override
        public String toString() {
            return "THREAD SUSPENDED: " + getDMContext() + " (" + fReason + ")"; 
        }
    }
    
    @Immutable 
    private static class VMSuspendedEvent extends AbstractDMEvent<IExecutionDMContext> 
        implements IContainerSuspendedDMEvent
    {
        private final StateChangeReason fReason;

        final private IExecutionDMContext[] fTriggeringThreads;  
        
        VMSuspendedEvent(PDAVirtualMachineDMContext ctx, PDAThreadDMContext threadCtx, StateChangeReason reason) { 
            super(ctx);
            fReason = reason;
            if (threadCtx != null) {
                fTriggeringThreads = new IExecutionDMContext[] { threadCtx };
            } else {
                fTriggeringThreads = EMPTY_TRIGGERING_CONTEXTS_ARRAY;
            }
        }
    
        @Override
	public StateChangeReason getReason() {
            return fReason;
        }

        @Override
	public IExecutionDMContext[] getTriggeringContexts() {
            return fTriggeringThreads;
        }


        @Override
        public String toString() {
            return "THREAD SUSPENDED: " + getDMContext() + 
                " (" + fReason + 
                ", trigger = " + Arrays.asList(fTriggeringThreads) +
                ")"; 
        }
    }

    @Immutable 
    private static class ExecutionDMData implements IExecutionDMData {
        private final StateChangeReason fReason;
        ExecutionDMData(StateChangeReason reason) {
            fReason = reason;
        }
        @Override
	public StateChangeReason getStateChangeReason() { return fReason; }
    }
    
    private static class ThreadStartedEvent extends AbstractDMEvent<IExecutionDMContext> 
        implements IStartedDMEvent 
    {
        ThreadStartedEvent(PDAThreadDMContext threadCtx) {
            super(threadCtx);
        }
    }
    
    private static class ThreadExitedEvent extends AbstractDMEvent<IExecutionDMContext> 
        implements IExitedDMEvent 
    {
        ThreadExitedEvent(PDAThreadDMContext threadCtx) {
            super(threadCtx);
        }
    }
    
    // Services 
    private PDACommandControl fCommandControl;

    // Reference to the virtual machine data model context
    private PDAVirtualMachineDMContext fDMContext;

    // VM state flags
	private boolean fVMSuspended = true;
    private boolean fVMResumePending = false;
    private boolean fVMSuspendPending = false;
	private boolean fVMStepping = false;
	private StateChangeReason fVMStateChangeReason = StateChangeReason.UNKNOWN;
	
	// Threads' state data 
    private static class ThreadInfo {
        final PDAThreadDMContext fContext;
        boolean fSuspended = false;
        boolean fResumePending = false;
        boolean fSuspendPending = false;
        boolean fStepping = false;
        StateChangeReason fStateChangeReason = StateChangeReason.UNKNOWN;        

        ThreadInfo(PDAThreadDMContext context) {
            fContext = context;
        }
        
        @Override
        public String toString() {
            return fContext.toString() + " (" +
                (fSuspended ? "SUSPENDED, " : "SUSPENDED, ") +
                fStateChangeReason + 
                (fResumePending ? ", RESUME_PENDING, " : "") +
                (fSuspendPending ? ", SUSPEND_PENDING, " : "") +
                (fStepping ? ", SUSPEND_PENDING, " : "") +
                ")";
            		
        }
    }

	private Map<Integer,  ThreadInfo> fThreads = new LinkedHashMap<Integer,  ThreadInfo>();
	
	
    public PDARunControl(DsfSession session) {
        super(session);
    }
    
    @Override
    protected BundleContext getBundleContext() {
        return PDAPlugin.getBundleContext();
    }
    
    @Override
    public void initialize(final RequestMonitor rm) {
        super.initialize(
            new RequestMonitor(getExecutor(), rm) { 
                @Override
                protected void handleSuccess() {
                    doInitialize(rm);
                }});
    }

    private void doInitialize(final RequestMonitor rm) {
        // Cache a reference to the command control and the virtual machine context
        fCommandControl = getServicesTracker().getService(PDACommandControl.class);
        fDMContext = fCommandControl.getContext();

        // Create the main thread context.
        fThreads.put(
            1,
            new ThreadInfo(new PDAThreadDMContext(getSession().getId(), fDMContext, 1)));

        // Add the run control service as a listener to PDA events, to catch 
        // suspended/resumed/started/exited events from the command control.
        fCommandControl.addEventListener(this);
        
        // Add the run control service as a listener to service events as well, 
        // in order to process our own suspended/resumed/started/exited events.
        getSession().addServiceEventListener(this, null);
        
        // Register the service with OSGi
        register(new String[]{IRunControl.class.getName(), PDARunControl.class.getName()}, new Hashtable<String,String>());
        
        rm.done();
    }

    @Override
    public void shutdown(final RequestMonitor rm) {
        fCommandControl.removeEventListener(this);
        getSession().removeServiceEventListener(this);
        super.shutdown(rm);
    }
    
    @Override
    public void eventReceived(Object output) {
        if (!(output instanceof String)) return;
        String event = (String)output;
        
        int nameEnd = event.indexOf(' ');
        nameEnd = nameEnd == -1 ? event.length() : nameEnd;
        String eventName = event.substring(0, nameEnd);
        
        PDAThreadDMContext thread = null;
        StateChangeReason reason = StateChangeReason.UNKNOWN;
        if (event.length() > nameEnd + 1) {
            if ( Character.isDigit(event.charAt(nameEnd + 1)) ) {
                int threadIdEnd = event.indexOf(' ', nameEnd + 1);
                threadIdEnd = threadIdEnd == -1 ? event.length() : threadIdEnd;
                try {
                    int threadId = Integer.parseInt(event.substring(nameEnd + 1, threadIdEnd));
                    if (fThreads.containsKey(threadId)) {
                        thread = fThreads.get(threadId).fContext;
                    } else {
                        // In case where a suspended event follows directly a 
                        // started event, a thread may not be in the list of 
                        // known threads yet.  In this case create the
                        // thread context based on the ID.
                        thread = new PDAThreadDMContext(getSession().getId(), fDMContext, threadId);
                    }
                } catch (NumberFormatException e) {}
                if (threadIdEnd + 1 < event.length()) {
                    reason = parseStateChangeReason(event.substring(threadIdEnd + 1));
                }
            } else {
                reason = parseStateChangeReason(event.substring(nameEnd + 1));
            }
        }
        
        // Handle PDA debugger suspended/resumed events and issue the 
        // corresponding Data Model events.  Do not update the state
        // information until we start dispatching the service events.
        IDMEvent<?> dmEvent = null;
        if (eventName.equals("suspended") && thread != null) {
            dmEvent = new ThreadSuspendedEvent(thread, reason);
        } else if (eventName.equals("resumed") && thread != null) {
            dmEvent = new ThreadResumedEvent(thread, reason);
        } else if (event.startsWith("vmsuspended")) {
            dmEvent = new VMSuspendedEvent(fDMContext, thread, reason);
        } else if (event.startsWith("vmresumed")) {
            dmEvent = new VMResumedEvent(fDMContext, reason);
        } else if (event.startsWith("started") && thread != null) {
            dmEvent = new ThreadStartedEvent(thread);
        } else if (event.startsWith("exited") && thread != null) {
            dmEvent = new ThreadExitedEvent(thread);
        }
        
        if (dmEvent != null) {
            getSession().dispatchEvent(dmEvent, getProperties());
        }
    }
    
    private StateChangeReason parseStateChangeReason(String reasonString) {
        if (reasonString.startsWith("breakpoint") || reasonString.startsWith("watch")) {
            return StateChangeReason.BREAKPOINT;
        } else if (reasonString.equals("step") || reasonString.equals("drop")) {
            return StateChangeReason.STEP;
        } else if (reasonString.equals("client")) {
            return StateChangeReason.USER_REQUEST;
        } else if (reasonString.startsWith("event")) {
            return StateChangeReason.SIGNAL;
        } else {
            return StateChangeReason.UNKNOWN;
        } 

    }
    
    @DsfServiceEventHandler 
    public void eventDispatched(ThreadResumedEvent e) {
        ThreadInfo info = fThreads.get(((PDAThreadDMContext)e.getDMContext()).getID());
        if (info != null) {
            info.fSuspended = false;
            info.fResumePending = false;
            info.fStateChangeReason = e.getReason();
            info.fStepping = e.getReason().equals(StateChangeReason.STEP);
        }
    }    


    @DsfServiceEventHandler 
    public void eventDispatched(VMResumedEvent e) {
        fVMSuspended = false;
        fVMResumePending = false;
        fVMStateChangeReason = e.getReason();
        fVMStepping = e.getReason().equals(StateChangeReason.STEP);
        for (ThreadInfo info : fThreads.values()) {
            info.fSuspended = false;
            info.fStateChangeReason = StateChangeReason.CONTAINER;
            info.fStepping = false;
        }
    }    

    @DsfServiceEventHandler 
    public void eventDispatched(ThreadSuspendedEvent e) {
        ThreadInfo info = fThreads.get(((PDAThreadDMContext)e.getDMContext()).getID());
        if (info != null) {
            info.fSuspended = true;
            info.fSuspendPending = false;
            info.fStateChangeReason = e.getReason();
            info.fStepping = e.getReason().equals(StateChangeReason.STEP);
        }
    }
    

    @DsfServiceEventHandler 
    public void eventDispatched(VMSuspendedEvent e) {
        fVMStateChangeReason = e.getReason();
        fVMSuspendPending = false;
        fVMSuspended = true;
        fVMStepping = false;
        List<IExecutionDMContext> triggeringContexts = Arrays.asList(e.getTriggeringContexts());
        for (ThreadInfo info : fThreads.values()) {
            info.fSuspended = true;
            info.fStateChangeReason = triggeringContexts.contains(info.fContext) 
                ? StateChangeReason.STEP : StateChangeReason.CONTAINER;
            info.fStepping = false;
        }        
    }
    
    @DsfServiceEventHandler 
    public void eventDispatched(ThreadStartedEvent e) {
        PDAThreadDMContext threadCtx = (PDAThreadDMContext)e.getDMContext();
        fThreads.put(threadCtx.getID(), new ThreadInfo(threadCtx));
    }    
    
    @DsfServiceEventHandler 
    public void eventDispatched(ThreadExitedEvent e) {
        PDAThreadDMContext threadCtx = (PDAThreadDMContext)e.getDMContext();
        fThreads.remove(threadCtx.getID());
    }    
    
    @Override
    public void canResume(IExecutionDMContext context, DataRequestMonitor<Boolean> rm) {
        rm.setData(doCanResume(context));
        rm.done();
    }
    
    private boolean doCanResume(IExecutionDMContext context) {
        if (context instanceof PDAThreadDMContext) {
            PDAThreadDMContext threadContext = (PDAThreadDMContext)context; 
            // Threads can be resumed only if the VM is not suspended.
            if (!fVMSuspended) { 
                ThreadInfo state = fThreads.get(threadContext.getID());
                if (state != null) {
                    return state.fSuspended && !state.fResumePending;
                }
            }
        } else {
            return fVMSuspended && !fVMResumePending;
        }
        return false;
    }
    
    private boolean doCanStep(IExecutionDMContext context, StepType stepType) {
        if (stepType == StepType.STEP_OVER || stepType == StepType.STEP_RETURN) {
            if (context instanceof PDAThreadDMContext) {
                PDAThreadDMContext threadContext = (PDAThreadDMContext)context; 
                // Only threads can be stepped.  But they can be stepped
                // while the VM is suspended or when just the thread is suspended.
                ThreadInfo state = fThreads.get(threadContext.getID());
                if (state != null) {
                    return state.fSuspended && !state.fResumePending;
                }
            }
        }
        return false;        
    }

    @Override
    public void canSuspend(IExecutionDMContext context, DataRequestMonitor<Boolean> rm) {
        rm.setData(doCanSuspend(context));
        rm.done();
    }
    
    private boolean doCanSuspend(IExecutionDMContext context) {
        if (context instanceof PDAThreadDMContext) {
            PDAThreadDMContext threadContext = (PDAThreadDMContext)context; 
            // Threads can be resumed only if the VM is not suspended.
            if (!fVMSuspended) { 
                ThreadInfo state = fThreads.get(threadContext.getID());
                if (state != null) {
                    return !state.fSuspended && !state.fSuspendPending;
                }
            }
        } else {
            return !fVMSuspended && !fVMSuspendPending;
        }
        return false;
    }

	@Override
	public boolean isSuspended(IExecutionDMContext context) {
        if (context instanceof PDAThreadDMContext) {
            PDAThreadDMContext threadContext = (PDAThreadDMContext)context; 
            // Threads can be resumed only if the VM is not suspended.
            if (!fVMSuspended) { 
                ThreadInfo state = fThreads.get(threadContext.getID());
                if (state != null) {
                    return state.fSuspended;
                }
            }
        } 
		return fVMSuspended;
	}

	@Override
	public boolean isStepping(IExecutionDMContext context) {
	    if (!isSuspended(context)) {
            if (context instanceof PDAThreadDMContext) {
                PDAThreadDMContext threadContext = (PDAThreadDMContext)context; 
                // Threads can be resumed only if the VM is not suspended.
                if (!fVMStepping) { 
                    ThreadInfo state = fThreads.get(threadContext.getID());
                    if (state != null) {
                        return state.fStepping;
                    }
                } 
            } 
            return fVMStepping;
	    }
	    return false;
    }

	@Override
	public void resume(IExecutionDMContext context, final RequestMonitor rm) {
		assert context != null;

		if (doCanResume(context)) { 
            if (context instanceof PDAThreadDMContext) {
                final PDAThreadDMContext threadCtx = (PDAThreadDMContext)context;
                fThreads.get(threadCtx.getID()).fResumePending = true;
                fCommandControl.queueCommand(
                    new PDAResumeCommand(threadCtx),
                    new DataRequestMonitor<PDACommandResult>(getExecutor(), rm) { 
                        @Override
                        protected void handleFailure() {
                            ThreadInfo threadState = fThreads.get(threadCtx.getID());
                            if (threadState != null) {
                                threadState.fResumePending = false;
                            }
                            super.handleFailure();
                        }
                    }
                );                
            } else {
                fVMResumePending = true;
                fCommandControl.queueCommand(
                	new PDAVMResumeCommand(fDMContext),
                	new DataRequestMonitor<PDACommandResult>(getExecutor(), rm) { 
                        @Override
                        protected void handleFailure() {
                            fVMResumePending = false;
                            super.handleFailure();
                        }
                	}
                );
            }
        } else {
            PDAPlugin.failRequest(rm, INVALID_STATE, "Given context: " + context + ", is already running.");
        }
	}
	
	@Override
	public void suspend(IExecutionDMContext context, final RequestMonitor rm){
		assert context != null;

		if (doCanSuspend(context)) {
            if (context instanceof PDAThreadDMContext) {
                final PDAThreadDMContext threadCtx = (PDAThreadDMContext)context;
                fThreads.get(threadCtx.getID()).fSuspendPending = true;
                fCommandControl.queueCommand(
                    new PDASuspendCommand(threadCtx),
                    new DataRequestMonitor<PDACommandResult>(getExecutor(), rm) { 
                        @Override
                        protected void handleFailure() {
                            ThreadInfo threadState = fThreads.get(threadCtx.getID());
                            if (threadState != null) {
                                threadState.fSuspendPending = false;
                            }
                            super.handleFailure();
                        }
                    }
                );
            } else {
                fVMSuspendPending = true; 
                fCommandControl.queueCommand(
                    new PDAVMSuspendCommand(fDMContext),
                    new DataRequestMonitor<PDACommandResult>(getExecutor(), rm) { 
                        @Override
                        protected void handleFailure() {
                            fVMSuspendPending = false;
                            super.handleFailure();
                        }
                    }
                );
            }
        } else {
            PDAPlugin.failRequest(rm, IDsfStatusConstants.INVALID_STATE, "Given context: " + context + ", is already suspended."); 
        }
    }
    
    @Override
    public void canStep(IExecutionDMContext context, StepType stepType, DataRequestMonitor<Boolean> rm) {
        rm.setData(doCanStep(context, stepType));
        rm.done();
    }
    
    @Override
    public void step(IExecutionDMContext context, StepType stepType, final RequestMonitor rm) {
    	assert context != null;
    	
    	if (doCanStep(context, stepType)) {
    	    final PDAThreadDMContext threadCtx = (PDAThreadDMContext)context;
            final boolean vmWasSuspneded = fVMSuspended;
    	    
    	    if (vmWasSuspneded) {
                fVMResumePending = true;
    	    } else {
    	        fThreads.get(threadCtx.getID()).fResumePending = true;
    	    }

    	    AbstractPDACommand<PDACommandResult> stepCommand = 
    	        stepType == StepType.STEP_RETURN 
    	            ? new PDAStepReturnCommand(threadCtx)
    	            : new PDAStepCommand(threadCtx);
    	           
    	    
            fCommandControl.queueCommand(
                stepCommand, 
                new DataRequestMonitor<PDACommandResult>(getExecutor(), rm) {
                    @Override
                    protected void handleFailure() {
                        // If the step command failed, we no longer
                        // expect to receive a resumed event.
                        if (vmWasSuspneded) {
                            fVMResumePending = false;
                        } else {
                            ThreadInfo threadState = fThreads.get(threadCtx.getID());
                            if (threadState != null) {
                                threadState.fResumePending = false;
                            }
                        }
                    }
                });

    	} else {
            PDAPlugin.failRequest(rm, INVALID_STATE, "Cannot resume context"); 
            return;
        }
    }

    @Override
    public void getExecutionContexts(final IContainerDMContext containerDmc, final DataRequestMonitor<IExecutionDMContext[]> rm) {
        IExecutionDMContext[] threads = new IExecutionDMContext[fThreads.size()];
        int i = 0;
        for (ThreadInfo info : fThreads.values()) {
            threads[i++] = info.fContext;
        }
        rm.setData(threads);
        rm.done();
    }
    
	@Override
	public void getExecutionData(IExecutionDMContext dmc, DataRequestMonitor<IExecutionDMData> rm) {
	    if (dmc instanceof PDAThreadDMContext) {
	        ThreadInfo info = fThreads.get(((PDAThreadDMContext)dmc).getID());
	        if (info == null) {
                PDAPlugin.failRequest(rm, INVALID_HANDLE, "Unknown DMC type");
	            return;
	        } 
            rm.setData( new ExecutionDMData(info.fStateChangeReason) );
	    } else {
	        rm.setData( new ExecutionDMData(fVMStateChangeReason) );
	    }
        rm.done();
    }
}
